﻿using System;
using System.Collections.Generic;
using System.Threading;
using System.Net;
using Sencha.Serialization;

#pragma warning disable 420
namespace Sencha.Direct.Client
{
	/// <summary>
	/// Client implementation on Ext Direct(http://www.sencha.com/products/extjs/extdirect) protocol.
	/// By default it will compose batches and make request in background. You can disable this behavior by setting <see cref="RpcConnection.Batched"/> to false. 
	/// </summary>
	public abstract partial class RpcConnection
	{
		private static readonly Dictionary<string, object> EmptyArgs = new Dictionary<string, object>();
		private static readonly int DispatchPeriod = 1000; // 1s

		private Uri m_routerUri;
		private volatile Batch m_currentBatch;
		private readonly Timer m_dispatchTimer;
		private int m_lastTransactionId = 1;
		private int m_requestTimeout;

		/// <summary>
		/// Request timeout for whole batch. Default value is 2min
		/// </summary>
		/// <exception cref="ArgumentOutOfRangeException">When timeout is lesser of -1. Set to System.Threading.Timeout.Infinite to disable timeout</exception>
		public int RequestTimeout { get { return m_requestTimeout; } set { if (value < -1) throw new ArgumentOutOfRangeException("value"); m_requestTimeout = value; } }
		/// <summary>
		/// Set or get batching behavior of connection. Set to false to disable batched request
		/// </summary>
		public bool Batched { get; set; }
		/// <summary>
		/// Direct Router address
		/// </summary>
		public Uri RouterUri { get { return m_routerUri; } }

		/// <summary>
		/// Constructor
		/// </summary>
		/// <exception cref="ArgumentNullException">When <paramref name="routerUri"/> is null</exception>
		protected RpcConnection(Uri routerUri)
		{
			if (routerUri == null)
				throw new ArgumentNullException("routerUri");

			m_routerUri = routerUri;
			this.Batched = true;
#if DEBUG
			m_requestTimeout = System.Threading.Timeout.Infinite;
#else
            m_requestTimeout = 120000;
#endif
			m_dispatchTimer = new Timer(new TimerCallback(Dispatch));
		}

		/// <summary>
		/// Execute <paramref name="action"/> on <paramref name="controller"/> with <paramref name="args"/>. 
		/// And call <paramref name="callback"/> upon completion
		/// </summary>
		/// <param name="controller">Controller name. Not null or empty string</param>
		/// <param name="action">Action name on controller. Not null or empty string</param>
		/// <param name="args">Method arguments. Can be null</param>
		/// <param name="callback">Callback function. Can be null</param>
		/// <param name="state">Callback state which is passed to callback function</param>
		/// <returns>Request async state</returns>
		/// <exception cref="ArgumentNullException"></exception>
		protected IAsyncResult BeginRequest(string controller, string action, IDictionary<string, object> args, Type returnType, AsyncCallback callback, object state)
		{
			if (string.IsNullOrEmpty(controller))
				throw new ArgumentNullException("controller");
			if (string.IsNullOrEmpty(action))
				throw new ArgumentNullException("action");
			if (args == null)
				args = EmptyArgs;
			if (returnType == null)
				returnType = typeof(object);

			var tid = System.Threading.Interlocked.Increment(ref m_lastTransactionId);

			// setup new transaction
			var request = new OutgoingRequest
			{
				Arguments = args ?? OutgoingRequest.EmptyArguments,
				TransactionId = tid,
				Type = "rpc",
				Action = action,
				Controller = controller
			};

			var asyncResult = this.BatchRequestAsync(request, returnType, callback, state);

			return asyncResult;
		}

		/// <summary>
		/// Execute <paramref name="action"/> on <paramref name="controller"/> with <paramref name="arguments"/>. 
		/// And call <paramref name="callback"/> upon completion
		/// </summary>
		/// <param name="controller">Controller name. Not null or empty string</param>
		/// <param name="action">Action name on controller. Not null or empty string</param>
		/// <param name="arguments">Method arguments. Can be null</param>
		/// <exception cref="ArgumentNullException"></exception>
		protected object Request(string controller, string action, IDictionary<string, object> arguments, Type returnType)
		{
			if (string.IsNullOrEmpty(controller))
				throw new ArgumentNullException("controller");
			if (string.IsNullOrEmpty(action))
				throw new ArgumentNullException("action");
			if (arguments == null)
				arguments = EmptyArgs;
			if (returnType == null)
				returnType = typeof(object);

			var tid = System.Threading.Interlocked.Increment(ref m_lastTransactionId);

			// setup new transaction
			var request = new OutgoingRequest
			{
				Arguments = arguments,
				TransactionId = tid,
				Type = "rpc",
				Action = action,
				Controller = controller
			};

			var result = this.BatchRequestSync(request, returnType);
			return result;
		}

		/// <summary>
		/// Finish async request in current thread
		/// </summary>
		/// <param name="result">Request result</param>
		protected object EndRequest(IAsyncResult result)
		{
			if (result == null)
				throw new ArgumentNullException("result");

			var asyncResult = result as AsyncRequestResult;

			if (asyncResult == null)
				throw new ArgumentException("Argument is not valid async result");

			if (asyncResult.Batch == null)
				throw new ArgumentException("Request is in invalid state. Batch is not started for this request (even when Batched=false it will create batch per request)");

			asyncResult.Batch.EndInvoke();

			if (asyncResult.Error != null)
			{
				ReflectionExtentions.TryToPreserveStackTrace(asyncResult.Error);
				throw asyncResult.Error;
			}

			var resultJson = asyncResult.Result;
			var resultData = this.Deserialize(asyncResult.ResultType, resultJson);

			return resultData;
		}

		/// <summary>
		/// Abort pending request
		/// </summary>
		/// <returns>true - when request successfully aborted, false - when request can't be aborted in current state(finished or already aborted)</returns>
		protected bool AbortRequest(IAsyncResult result)
		{
			if (result == null)
				throw new ArgumentNullException("result");

			var asyncResult = result as AsyncRequestResult;

			if (asyncResult == null)
				throw new ArgumentException("Argument is not valid asyncResult result");

			if (asyncResult.Batch == null)
				throw new ArgumentException("Request is in invalid state. Batch is not set");

			var isAborted = asyncResult.Batch.Abort(asyncResult);

			return isAborted;
		}

		private object BatchRequestSync(OutgoingRequest request, Type returnType)
		{
			var batch = new Batch(this);
			batch.Add(request, returnType, null, null);

			batch.Execute(true);

			var asyncResult = request.AsyncResult;

			if (asyncResult.Error != null)
			{
				ReflectionExtentions.TryToPreserveStackTrace(asyncResult.Error);
				throw asyncResult.Error;
			}

			var resultJson = asyncResult.Result;            
			var resultData = this.Deserialize(asyncResult.ResultType, resultJson);

			return resultData;
		}
		private IAsyncResult BatchRequestAsync(OutgoingRequest request, Type resultType, AsyncCallback callback, object state)
		{
			var asyncResult = (IAsyncResult)null;

			if (!this.Batched)
			{
				var batch = new Batch(this);
				asyncResult = batch.Add(request, resultType, callback, state);
				batch.Execute(false);
			}
			else
			{
				if (m_currentBatch == null)
					m_currentBatch = Interlocked.CompareExchange(ref m_currentBatch, new Batch(this), null) ?? m_currentBatch;

				lock (m_currentBatch)
				{
					asyncResult = m_currentBatch.Add(request, resultType, callback, state);
					m_dispatchTimer.Change(DispatchPeriod, Timeout.Infinite);
				}
			}

			return asyncResult;
		}

		private void Dispatch(object state)
		{
			var batch = state as Batch;
			if (batch == null)
				batch = Interlocked.Exchange(ref m_currentBatch, null);

			if (batch == null || batch.Requests.Count == 0)
				return; // where nothink to send, so sleep up till next timer's tick

			batch.Execute(false);
		}

		protected internal virtual object Deserialize(Type type, JsonString json)
		{
			if (type == null)
				throw new ArgumentNullException("type");
            if (json == null)
                return null;

			var serializer = new JsonSerializer(type);
			return serializer.ReadObject<object>(json);
		}

		/// <summary>
		/// String representation on connection state
		/// </summary>
		public override string ToString()
		{
			return string.Format("{{rest: {0}}}", this.RouterUri);
		}
	}
}
